package edu.northwestern.cbits.purple_robot_manager.scripting; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Scanner; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.javascript.Context; import org.mozilla.javascript.EcmaError; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeJSON; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import android.content.res.AssetManager; import android.os.Bundle; import android.util.Log; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.ScheduleManager; import edu.northwestern.cbits.purple_robot_manager.annotation.ScriptingEngineMethod; import edu.northwestern.cbits.purple_robot_manager.config.JSONConfigFile; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.models.ModelManager; import edu.northwestern.cbits.purple_robot_manager.probes.features.Feature; import edu.northwestern.cbits.purple_robot_manager.probes.features.JavascriptFeature; public class JavaScriptEngine extends BaseScriptEngine { private Context _jsContext = null; private Scriptable _scope = null; public JavaScriptEngine(android.content.Context context) { super(context); } @ScriptingEngineMethod(language = "JavaScript", assetPath = "js_run_script.html", category = R.string.docs_script_category_system_integration, arguments = { "script", "extras", "extraValues" }) public Object runScript(String script) throws EvaluatorException, EcmaError { return this.runScript(script, null, null); } @SuppressWarnings("unchecked") @ScriptingEngineMethod(language = "JavaScript", assetPath = "js_run_script.html", category = R.string.docs_script_category_system_integration, arguments = { "script", "extras", "extraValues" }) public Object runScript(String script, String extrasName, Object extras) throws EvaluatorException, EcmaError { this._jsContext = Context.enter(); this._jsContext.setOptimizationLevel(-1); this._scope = _jsContext.initStandardObjects(); Object thisWrapper = Context.javaToJS(this, this._scope); ScriptableObject.putProperty(this._scope, "PurpleRobot", thisWrapper); if (extras instanceof Map<?, ?>) { extras = JavaScriptEngine.mapToNative(this._jsContext, this._scope, (Map<String, Object>) extras); extras = NativeJSON.stringify(this._jsContext, this._scope, extras, null, null); } if (extras != null && extrasName != null) script = "var " + extrasName + " = " + extras.toString() + "; " + script; return this._jsContext.evaluateString(this._scope, script, "<engine>", 0, null); } @ScriptingEngineMethod(language = "All", assetPath = "all_log.html", category = R.string.docs_script_category_diagnostic, arguments = { "message" }) public boolean log(String event, NativeObject params) { if (params != null) return LogManager.getInstance(this._context).log(event, JavaScriptEngine.nativeToMap(params)); return LogManager.getInstance(this._context).log(event, new HashMap<String, Object>()); } @SuppressWarnings("resource") @ScriptingEngineMethod(language = "JavaScript", assetPath = "js_load_library.html", category = R.string.docs_script_category_system_integration, arguments = { "libraryName" }) public boolean loadLibrary(String libraryName) { if (this._jsContext != null && this._scope != null) { try { if (libraryName.endsWith(".js") == false) libraryName += ".js"; AssetManager am = this._context.getAssets(); InputStream jsStream = am.open("js/" + libraryName); // http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string Scanner s = new Scanner(jsStream).useDelimiter("\\A"); String script = ""; if (s.hasNext()) script = s.next(); else return false; this._jsContext.evaluateString(this._scope, script, "<engine>", 1, null); return true; } catch (IOException e) { LogManager.getInstance(this._context).logException(e); } } return false; } @ScriptingEngineMethod(language = "JavaScript") public void updateWidget(final NativeObject parameters) { this.updateWidget(JavaScriptEngine.nativeToMap(parameters)); } @ScriptingEngineMethod(language = "All", assetPath = "all_namespaces.html", category = R.string.docs_script_category_persistence, arguments = { }) public NativeArray namespaces() { List<String> namespaces = super.namespaceList(); NativeArray allNamespaces = new NativeArray(namespaces.size()); for (int i = 0; i < namespaces.size(); i++) { NativeArray.putProperty(allNamespaces, i, namespaces.get(i)); } return allNamespaces; } @ScriptingEngineMethod(language = "All", assetPath = "all_scheduled_scripts.html", category = R.string.docs_script_category_dialogs_notifications, arguments = { }) public NativeArray scheduledScripts() { List<Map<String, String>> scripts = ScheduleManager.allScripts(this._context); NativeArray allScripts = new NativeArray(scripts.size()); for (int i = 0; i < scripts.size(); i++) { Map<String, String> script = scripts.get(i); Map<String, Object> scriptObj = new HashMap<>(); for (String key : script.keySet()) scriptObj.put(key, script.get(key)); NativeObject nativeObj = JavaScriptEngine.mapToNative(this._jsContext, this._scope, scriptObj); NativeArray.putProperty(allScripts, i, nativeObj); } return allScripts; } @ScriptingEngineMethod(language = "All", assetPath = "all_broadcast_intent.html", category = R.string.docs_script_category_system_integration, arguments = { "action", "extras" }) public boolean broadcastIntent(final String action, final NativeObject extras) { return this.broadcastIntent(action, JavaScriptEngine.nativeToMap(extras)); } @ScriptingEngineMethod(language = "JavaScript") public boolean updateWidget(final String title, final String message, final String applicationName, final NativeObject launchParams, final String script) { return this.updateWidget(title, message, applicationName, JavaScriptEngine.nativeToMap(launchParams), script); } @ScriptingEngineMethod(language = "All", assetPath = "all_launch_application.html", category = R.string.docs_script_category_system_integration, arguments = { "applicationName", "options", "script" }) public boolean launchApplication(String applicationName, final NativeObject launchParams, final String script) { return this.launchApplication(applicationName, JavaScriptEngine.nativeToMap(launchParams), script); } @ScriptingEngineMethod(language = "All", assetPath = "all_show_app_launch_note.html", category = R.string.docs_script_category_dialogs_notifications, arguments = { "title", "message", "packageName" }) public boolean showApplicationLaunchNotification(String title, String message, String applicationName, boolean persistent, final NativeObject launchParams, final String script) { return this.showApplicationLaunchNotification(title, message, applicationName, persistent, JavaScriptEngine.nativeToMap(launchParams), script); } @ScriptingEngineMethod(language = "All", assetPath = "all_show_app_launch_note.html", category = R.string.docs_script_category_dialogs_notifications, arguments = { "title", "message", "packageName" }) public boolean showApplicationLaunchNotification(String title, String message, String applicationName, final NativeObject launchParams, final String script) { return this.showApplicationLaunchNotification(title, message, applicationName, JavaScriptEngine.nativeToMap(launchParams), script); } @ScriptingEngineMethod(language = "All", assetPath = "all_update_trigger.html", category = R.string.docs_script_category_triggers_automation, arguments = { "identifier", "definition" }) public boolean updateTrigger(String triggerId, NativeObject nativeJson) { return this.updateTrigger(triggerId, JavaScriptEngine.nativeToMap(nativeJson)); } @ScriptingEngineMethod(language = "All", assetPath = "all_emit_reading.html", category = R.string.docs_script_category_data_collection, arguments = { "name", "value", "priority" }) public void emitReading(String name, Object value) { this.emitReading(name, value, false); } @ScriptingEngineMethod(language = "All", assetPath = "all_emit_reading.html", category = R.string.docs_script_category_data_collection, arguments = { "name", "value", "priority" }) public void emitReading(String name, Object value, boolean priority) { double now = System.currentTimeMillis(); Bundle bundle = new Bundle(); bundle.putString("PROBE", name); bundle.putDouble("TIMESTAMP", now / 1000); if (priority) bundle.putBoolean("PRIORITY", true); if (value instanceof String) bundle.putString(Feature.FEATURE_VALUE, value.toString()); else if (value instanceof Double) { Double d = (Double) value; bundle.putDouble(Feature.FEATURE_VALUE, d); } else if (value instanceof NativeObject) { NativeObject nativeObj = (NativeObject) value; Bundle b = JavascriptFeature.bundleForNativeObject(nativeObj); bundle.putParcelable(Feature.FEATURE_VALUE, b); } else { Log.e("PR", "JS PLUGIN GOT UNKNOWN VALUE " + value); if (value != null) Log.e("PR", "JS PLUGIN GOT UNKNOWN CLASS " + value.getClass()); } this.transmitData(bundle); } public static JSONArray nativeToJson(NativeArray nativeArray) throws JSONException { JSONArray array = new JSONArray(); for (int i = 0; i < nativeArray.getLength(); i++) { Object value = nativeArray.get(i); if (value instanceof String) array.put(value); else if (value instanceof Double) array.put(value); else if (value instanceof NativeObject) { NativeObject obj = (NativeObject) value; array.put(JavaScriptEngine.nativeToJson(obj)); } else if (value instanceof NativeArray) { NativeArray arr = (NativeArray) value; array.put(JavaScriptEngine.nativeToJson(arr)); } } return array; } public static JSONObject nativeToJson(NativeObject nativeObj) throws JSONException { if (nativeObj == null) return null; JSONObject json = new JSONObject(); for (Entry<Object, Object> e : nativeObj.entrySet()) { String key = e.getKey().toString(); Object value = e.getValue(); if (value instanceof String) json.put(key, value); else if (value instanceof Double) json.put(key, value); else if (value instanceof NativeObject) { NativeObject obj = (NativeObject) value; json.put(key, JavaScriptEngine.nativeToJson(obj)); } else if (value instanceof NativeArray) { NativeArray arr = (NativeArray) value; json.put(key, JavaScriptEngine.nativeToJson(arr)); } } return json; } @Override protected String language() { return "JavaScript"; } public static boolean canRun(String script) { // TODO: Validate if actually JavaScript... return true; } public static Map<String, Object> nativeToMap(NativeObject nativeObj) { HashMap<String, Object> params = new HashMap<>(); for (Entry<Object, Object> e : nativeObj.entrySet()) { Object value = e.getValue(); if (value instanceof NativeObject) value = JavaScriptEngine.nativeToMap((NativeObject) value); else if (value instanceof NativeArray) value = JavaScriptEngine.nativeToArrayList((NativeArray) value); params.put(e.getKey().toString(), value); } return params; } private static ArrayList<Object> nativeToArrayList(NativeArray array) { ArrayList<Object> list = new ArrayList<>(); for (int i = 0; i < array.size(); i++) { Object value = array.get(i); if (value instanceof NativeObject) value = JavaScriptEngine.nativeToMap((NativeObject) value); else if (value instanceof NativeArray) value = JavaScriptEngine.nativeToArrayList((NativeArray) value); list.add(value); } return list; } @SuppressWarnings("unchecked") private static NativeObject mapToNative(Context context, Scriptable scope, Map<String, Object> map) { NativeObject obj = (NativeObject) context.newObject(scope); for (String key : map.keySet()) { Object value = map.get(key); if (value instanceof Map) value = JavaScriptEngine.mapToNative(context, scope, (Map<String, Object>) value); else if (value instanceof Long) { Long l = (Long) value; value = l.doubleValue(); } else if (value instanceof List) { List list = (List) value; NativeArray array = new NativeArray(list.size()); for (int i = 0; i < list.size(); i++) { Object item = list.get(i); if (item instanceof Map) item = JavaScriptEngine.mapToNative(context, scope, (Map<String, Object>) item); NativeArray.putProperty(array, i, item); } value = array; } NativeObject.putProperty(obj, key, value); } return obj; } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_config.html", category = R.string.docs_script_category_configuration, arguments = { }) public String fetchConfig() { JSONConfigFile config = new JSONConfigFile(this._context); return config.toString(); } @ScriptingEngineMethod(language = "All", assetPath = "all_update_config_map.html", category = R.string.docs_script_category_configuration, arguments = { "configMap" }) public boolean updateConfig(NativeObject nativeObj) { Map<String, Object> paramsMap = JavaScriptEngine.nativeToMap(nativeObj); return super.updateConfig(paramsMap); } public NativeArray fetchNamespaces() { return this.namespaces(); } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_trigger_ids.html", category = R.string.docs_script_category_triggers_automation, arguments = { }) public NativeArray fetchTriggerIds() { List<String> triggerIds = super.fetchTriggerIdList(); String[] values = new String[triggerIds.size()]; for (int i = 0; i < triggerIds.size(); i++) { values[i] = triggerIds.get(i); } return new NativeArray(values); } @ScriptingEngineMethod(language = "All", assetPath = "all_list_tones.html", category = R.string.docs_script_category_dialogs_notifications, arguments = { }) public NativeArray listTones() { List<String> tones = super.fetchToneList(); String[] values = new String[tones.size()]; for (int i = 0; i < tones.size(); i++) { values[i] = tones.get(i); } return new NativeArray(values); } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_snapshot_ids.html", category = R.string.docs_script_category_data_collection, arguments = { }) public NativeArray fetchSnapshotIds() { List<String> snapshotIds = super.fetchSnapshotIdList(); String[] values = new String[snapshotIds.size()]; for (int i = 0; i < snapshotIds.size(); i++) { values[i] = snapshotIds.get(i); } return new NativeArray(values); } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_snapshot.html", category = R.string.docs_script_category_data_collection, arguments = { "snapshotId" }) public NativeObject fetchSnapshot(String timestamp) { Map<String, Object> snapshot = super.fetchSnapshotMap(timestamp); if (snapshot != null) return JavaScriptEngine.mapToNative(this._jsContext, this._scope, snapshot); return null; } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_namespace.html", category = R.string.docs_script_category_persistence, arguments = { "namespaceId" }) public NativeObject fetchNamespace(String namespace) { Map<String, Object> map = super.fetchNamespaceMap(namespace); return JavaScriptEngine.mapToNative(this._jsContext, this._scope, map); } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_trigger.html", category = R.string.docs_script_category_triggers_automation, arguments = { "triggerId" }) public NativeObject fetchTrigger(String id) { Map<String, Object> trigger = super.fetchTriggerMap(id); if (trigger != null) return JavaScriptEngine.mapToNative(this._jsContext, this._scope, trigger); return null; } // TODO: Document API... public NativeObject models() { Map<String, Object> modelMap = ModelManager.getInstance(this._context).models(this._context); return JavaScriptEngine.mapToNative(this._jsContext, this._scope, modelMap); } @ScriptingEngineMethod(language = "All", assetPath = "all_readings.html", category = R.string.docs_script_category_data_collection, arguments = { }) public NativeObject readings() { Map<String, Object> readings = ModelManager.getInstance(this._context).readings(this._context); return JavaScriptEngine.mapToNative(this._jsContext, this._scope, readings); } // TODO: Document API... public NativeObject predictions() { Map<String, Object> predictions = ModelManager.getInstance(this._context).predictions(this._context); return JavaScriptEngine.mapToNative(this._jsContext, this._scope, predictions); } @ScriptingEngineMethod(language = "All", assetPath = "all_fetch_labels.html", category = R.string.docs_script_category_data_collection, arguments = { "instructions", "labels" }) public void fetchLabels(String appContext, String instructions, final NativeObject labels) { this.fetchLabelsInterface(appContext, instructions, JavaScriptEngine.nativeToMap(labels)); } @ScriptingEngineMethod(language = "JavaScript") public NativeObject fetchWidget(String identifier) { Map<String, Object> map = super.fetchWidgetMap(identifier); return JavaScriptEngine.mapToNative(this._jsContext, this._scope, map); } @ScriptingEngineMethod(language = "JavaScript") public NativeArray widgets() { List<String> widgets = super.widgetsList(); String[] values = new String[widgets.size()]; for (int i = 0; i < widgets.size(); i++) { values[i] = widgets.get(i); } return new NativeArray(values); } @ScriptingEngineMethod(language = "All", assetPath = "all_update_probe.html", category = R.string.docs_script_category_data_collection, arguments = { "parameters" }) public boolean updateProbe(NativeObject params) { Map<String, Object> values = JavaScriptEngine.nativeToMap(params); return super.updateProbeConfig(values); } @ScriptingEngineMethod(language = "All", assetPath = "all_probe_config.html", category = R.string.docs_script_category_data_collection, arguments = { "probeName" }) public NativeObject probeConfig(String name) { Map<String, Object> map = this.probeConfigMap(name); return JavaScriptEngine.mapToNative(this._jsContext, this._scope, map); } }